package org.dfl.core.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.dfl.core.data.History;
import org.dfl.core.data.PureHistory;
import org.dfl.core.data.Token;
import org.dfl.core.data.HistoryDataMapping;
import org.dfl.core.data.connectors.TypeConnector;
import org.dfl.core.data.types.TypeContainer;
import org.dfl.core.model.listeners.PlaceListener;
import org.jdom.Element;

/**
 * a class that represents a place in a flow
 * 
 * @author Piotr Wlodarczyk
 * 
 */
public class Place implements IOElement {
	// identyfikator
	String id;
	// polaczenia wejsciowe do tego miejsca
	Collection<TOutputEdge> inputEdges = new HashSet<TOutputEdge>();
	// polaczenia wyjsciowe do tego miejsca
	Collection<TInputEdge> outputEdges = new HashSet<TInputEdge>();
	// tym miejsca - domyslnie trzyma AnyType
	TypeContainer typeContainer = new TypeContainer();
	// dane trzymane w tym miejscu - dane sa trzymane w mapie pod kluczem
	// histori (aby latwo bylo je laczyc)
	Map<History, Collection<Token>> historyDataMap = new HashMap<History, Collection<Token>>();
	Map<PureHistory, Collection<History>> nestedHistoryDataMap = new HashMap<PureHistory, Collection<History>>();
	// nasluchiwacze zmian w tym miejscu
	Set<PlaceListener> listeners = new HashSet<PlaceListener>();

	public TypeContainer getTypeContainer() {
		return typeContainer;
	}

	// id
	public String getId() {
		if (id == null) {
			regenerateId();
		}
		return id;
	}

	public void setId(String id) {
		if (!IdGenerator.isRegistered(id)) {
			IdGenerator.registerId(id);
			this.id = id;
		} else {
			regenerateId();
		}
	}

	protected void regenerateId() {
		this.id = IdGenerator.getGeneratedId(this.getClass());
	}

	// outputEdges
	public Collection<TInputEdge> getOutputEdges() {
		return outputEdges;
	}

	public boolean addOutputEdge(TInputEdge e, TypeConnector connector) {
		if (e.getTypeContainer().connectBy(connector)) {
			getOutputEdges().add(e);
			return true;
		} else {
			return false;
		}
	}

	public void removeOutputEdge(TInputEdge e) {
		getOutputEdges().remove(e);
	}

	// inputEdges
	public Collection<TOutputEdge> getInputEdges() {
		return inputEdges;
	}

	public boolean addInputEdge(TOutputEdge e, TypeConnector connector) {
		if (e.getTypeContainer().connectBy(connector)) {
			getInputEdges().add(e);
			return true;
		} else {
			return false;
		}
	}

	public void removeInputEdge(TOutputEdge e) {
		getInputEdges().remove(e);
	}

	public void removeToken(Token token) {
			HashSet<History> histories = new HashSet<History>(getHistoryDataMap().keySet());
			for (History h : histories) {
				if(getHistoryDataMap().containsKey(h)){
					getHistoryDataMap().get(h).remove(token);
					if (getHistoryDataMap().get(h).isEmpty()) {
						getHistoryDataMap().remove(h);
						PureHistory pH = PureHistory.getPureHistoryFor(h);
						nestedHistoryDataMap.get(pH).remove(h);
						if (nestedHistoryDataMap.get(pH).isEmpty()) {
							nestedHistoryDataMap.remove(pH);
						}
					}
				}
			}
			// powiadom
			fireGetData();
	}

	public void addToken(Token token) {
		History hist = token.getHistory();

		if (historyDataMap.containsKey(hist)) {
			historyDataMap.get(hist).add(token);
		} else {
			Collection<Token> col = new HashSet<Token>();
			col.add(token);
			historyDataMap.put(hist, col);
		}

		PureHistory pHist = PureHistory.getPureHistoryFor(hist);
		if (nestedHistoryDataMap.containsKey(pHist)) {
			nestedHistoryDataMap.get(pHist).add(hist);
		} else {
			Collection<History> col = new HashSet<History>();
			col.add(hist);
			nestedHistoryDataMap.put(pHist, col);
		}

		// dodano jakies dane - powiadom o tym
		fireSetData();
	}

	/**
	 * returns histories for tokens that can be extracted from this place by a
	 * given edge
	 * 
	 * @param e -
	 *            edge
	 * @return histories
	 */
	@SuppressWarnings("unchecked")
	public Collection<HistoryDataMapping> getFixedHistories(TInputEdge e) {
		Set<HistoryDataMapping> ret = new HashSet<HistoryDataMapping>();
		if (e.isNested()) {
			Collection<PureHistory> pHistories = nestedHistoryDataMap.keySet();

			for (PureHistory phistory : pHistories) {
				Collection<History> histories = nestedHistoryDataMap
						.get(phistory);

				// jesli potrzeba zlaczenia, to potrzebujemy odpowiedniej liczby
				// elementow
				Collection neededValues = new ArrayList(phistory.getCol());
				for (History h : histories) {
					Collection<Token> tokens = historyDataMap.get(h);
					for (Token t : tokens) {
						if (t.isActive())
							neededValues.remove(t.getHistory().getColElem());
					}
				}
				// mozna wyciagnac tylko te dane, ktore sa pelne
				// i jesli na krawedzi nie mamy filtru tylko pustych (po
				// zlaczeniu na pewno nie bedzie pustej kolekcji)
				if (neededValues.isEmpty() && !e.isNeededEmpty()) {
					ret.add(new HistoryDataMapping(phistory, phistory
							.getHistory()));
				}
			}
		} else {
			Collection<History> colHistories = historyDataMap.keySet();
			for (History history : colHistories) {
				Collection<Token> tokens = historyDataMap.get(history);

				// jesli potrzeba zlaczenia, to potrzebujemy odpowiedniej liczby
				// elementow
				// przepuszczamy dane tylko jesli pasuja :) - dla * zbieramy
				// wszystkie dane, zeby moc je potem polaczyc
				// sprawdzenie, czy choc 1 dane pasuja
				for (Token token : tokens) {
					if (token.isActive()
							&& !((e.isNeededEmpty() && !((Collection) token
									.getData()).isEmpty())
									|| (e.isNeededNotempty() && ((Collection) token
											.getData()).isEmpty())
									|| (e.isNeededFalse() && ((Boolean) token
											.getData()).booleanValue() == true) || (e
									.isNeededTrue() && ((Boolean) token
									.getData()).booleanValue() == false))) {
						ret.add(new HistoryDataMapping(history, history));
						break;
					}
				}
			}
		}
		return ret;
	}

	/**
	 * get token by the edge e with specific history
	 * 
	 * @param phistory -
	 *            history
	 * @param e -
	 *            edge
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public Token getToken(PureHistory phistory, TInputEdge e) {
		if (e.isNested()) {
			// wyciagnij klucz z historyDataMap odpowiedni do colH
			if (!nestedHistoryDataMap.containsKey(phistory))
				return null;
			Collection<History> histories = new ArrayList<History>(
					nestedHistoryDataMap.get(phistory));

			// nalezy wyciagnac historie oryginalna, ktorej ogonem jest h i jest
			// ok do wyciagniecia
			Collection<Object> newTokenValue = new ArrayList<Object>();
			Collection neededValues = new ArrayList(phistory.getCol());
			boolean forceGetFirstToken = neededValues.isEmpty();
			for (History h : histories) {
				Collection<Token> tokens = new ArrayList<Token>(historyDataMap
						.get(h));
				for (Token token : tokens) {
					if (token.isActive()
							&& (neededValues.contains(token.getHistory()
									.getColElem()) || forceGetFirstToken)) {
						neededValues.remove(h.getColElem());
						forceGetFirstToken = false;
						
						// remove token from place
						removeToken(token);
						newTokenValue.add(token.getData());
					}
				}
			}
			return new Token(newTokenValue, phistory.getHistory());
		} else {
			// phistory should be actually a History object over here!
			History history = (History) phistory;

			if (!historyDataMap.containsKey(history))
				return null;
			Collection<Token> tokens = new ArrayList<Token>(historyDataMap
					.get(history));
			for (Token token : tokens) {
				if (token.isActive()) {
					removeToken(token);
					return token;
				}
			}
			return null;
		}
	}

	/**
	 * remove all tokens in this place
	 */
	public void clearData() {
		historyDataMap.clear();
		nestedHistoryDataMap.clear();
		fireGetData();
	}

	// nasluchiwacze
	public void addListener(PlaceListener l) {
		listeners.add(l);
	}

	public void removeListener(PlaceListener l) {
		listeners.remove(l);
	}

	/**
	 * sets a token active or disabled
	 * 
	 * @param token
	 * @param isActive
	 */
	public void setTokenActive(Token token, boolean isActive) {
		token.setActive(isActive);
		if (isActive) {
			fireSetData();
		} else {
			fireGetData();
		}
	}

	protected void fireSetData() {
		Set<PlaceListener> ll = new HashSet<PlaceListener>(listeners);
		for (PlaceListener l : ll) {
			if (l != null)
				l.tokenAdded(this);
		}
	}

	protected void fireGetData() {
		Set<PlaceListener> ll = new HashSet<PlaceListener>(listeners);
		for (PlaceListener l : ll) {
			if (l != null)
				l.tokenRemoved(this, historyDataMap.size());
		}
	}

	public Map<History, Collection<Token>> getHistoryDataMap() {
		return historyDataMap;
	}

	public void load(Element pElem) throws XMLLoaderException {
		// getTypeContainer().load(pElem.getChild("toolspecific").getChild("type").getText());
		this.id = pElem.getAttributeValue("id"); // id ustawiany "recznie" -
		// bez sprawdzania idkow
		IdGenerator.registerId(this.id);
	}

	public Element save() {
		Element tElem = new Element("place");
		tElem.setAttribute("id", getId());
		// wykomentowane, bo nie bedziemy trzymac typu miejsca w xmlu - typ jest
		// budowany automatycznie w zaleznosci od polaczen
		// Element toolspecificElem = new Element("toolspecific");
		// toolspecificElem.setAttribute("tool","");
		// toolspecificElem.setAttribute("version","");
		// tElem.addContent(toolspecificElem);

		return tElem;
	}

	@Override
	public String toString() {
		return toString("");
	}

	public String toString(String substr) {
		return "<PLACE id='" + getId() + "' type='"
				+ getTypeContainer().getType().toString() + "'>" + substr
				+ "</PLACE>";
	}
}
